<!DOCTYPE html>
<!--
Tests that AudioBufferSourceNode supports loop-points with .loopStart and .loopEnd.
-->
<html>
  <head>
    <title>
      audiobuffersource-loop-points.html
    </title>
    <script src="../../resources/testharness.js"></script>
    <script src="../../resources/testharnessreport.js"></script>
    <script src="../resources/audit-util.js"></script>
    <script src="../resources/audit.js"></script>
    <script src="../resources/audio-file-utils.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let audit = Audit.createTaskRunner();

      // Use power of two to eliminate round-off in computing frames from time
      // and vice versa. 
      let sampleRate = 32768;
      let numberOfNotes = 60;  // play over a 5 octave range
      // Make sure noteDuration, noteSilence, and noteSpacing are exactly whole
      // frames.
      let noteDuration = Math.floor(0.025 * sampleRate) / sampleRate;
      // Leave about 5ms of silence between each "note"
      let noteSilence = Math.floor(0.005 * sampleRate) / sampleRate;
      let noteSpacing = noteDuration + noteSilence;
      let lengthInSeconds = numberOfNotes * noteSpacing;

      let context = 0;
      let expectedAudio;

      function createTestBuffer(frequency, sampleRate) {
        // Create a buffer containing two periods at this frequency.
        // The 1st half is a pure sine wave period scaled by a linear ramp from
        // 0 -> 1. The 2nd half of the buffer corresponds exactly to one pure
        // sine wave period.
        let onePeriodDuration = 1 / frequency;
        let sampleFrameLength = 2 * onePeriodDuration * sampleRate;

        let audioBuffer =
            context.createBuffer(1, sampleFrameLength, sampleRate);

        let n = audioBuffer.length;
        let channelData = audioBuffer.getChannelData(0);

        for (let i = 0; i < n; ++i) {
          let sample = Math.sin(frequency * 2.0 * Math.PI * i / sampleRate);

          // Linear ramp from 0 -> 1 for the first period.
          // Stay at 1 for the 2nd period.
          let scale = i < n / 2 ? i / (n / 2) : 1;
          sample *= scale;

          channelData[i] = sample;
        }

        return audioBuffer;
      }

      function playNote(buffer, time, duration, playbackRate) {
        let source = context.createBufferSource();
        source.buffer = buffer;
        source.playbackRate.value = playbackRate;

        let gainNode = context.createGain();
        source.connect(gainNode);
        gainNode.connect(context.destination);

        // Loop the 2nd half of the buffer.
        // We should be able to hear any problems as glitches if the looping
        // incorrectly indexes to anywhere outside of the desired loop-points,
        // since only the 2nd half is a perfect sine-wave cycle, while the 1st
        // half of the buffer contains a linear ramp of a sine-wave cycle.
        source.loop = true;
        source.loopStart = 0.5 * buffer.duration;
        source.loopEnd = buffer.duration;

        // Play for the given duration.
        source.start(time);
        source.stop(time + duration);

        // Apply a quick linear fade-out to avoid a click at the end of the
        // note.
        gainNode.gain.value = 1;
        gainNode.gain.setValueAtTime(1, time + duration - 0.005);
        gainNode.gain.linearRampToValueAtTime(0, time + duration);
      }

      audit.define(
          {
            label: 'initialize',
            description: 'Set up context and expected results'
          },
          async (task, should) => {
            // Create offline audio context.
            should(
                () => {context = new OfflineAudioContext(
                           2, sampleRate * lengthInSeconds, sampleRate)},
                'Creating context for testing')
                .notThrow();

            const arrayBuffer = await Audit.loadFileFromUrl(
              'resources/audiobuffersource-loop-points-expected.wav');
            expectedAudio = await context.decodeAudioData(arrayBuffer);
            task.done();
         });

      audit.define(
          {
            label: 'test',
            description: 'Test loop points and compare with expected results'
          },
          (task, should) => {
            // Create the test buffer.
            // We'll loop this with the loop-points set for the 2nd half of this
            // buffer.
            let buffer = createTestBuffer(440.0, sampleRate);

            // Play all the notes as a chromatic scale.
            for (let i = 0; i < numberOfNotes; ++i) {
              let time = i * noteSpacing;
              // start three octaves down
              let semitone = i - numberOfNotes / 2;

              // Convert from semitone to rate.
              let playbackRate = Math.pow(2, semitone / 12);

              playNote(buffer, time, noteDuration, playbackRate);
            }

            context.startRendering()
                .then(renderedAudio => {
                  // Compute a threshold based on the maximum error, |maxUlp|,
                  // in ULP. This is experimentally determined. Assuming that
                  // the reference file is a 16-bit wav file, the max values in
                  // the wave file are +/- 32768.
                  let maxUlp = 0;
                  let threshold = maxUlp / 32768;

                  for (let k = 0; k < renderedAudio.numberOfChannels; ++k) {
                    should(
                        renderedAudio.getChannelData(k),
                        'Rendered audio for channel ' + k)
                        .beCloseToArray(
                            expectedAudio.getChannelData(k),
                            {absoluteThreshold: threshold});
                  }

                  const filename = 'audiobuffersource-loop-points-actual.wav';
                  if (downloadAudioBuffer(renderedAudio, filename, true)) {
                    should(true, 'Saved reference file').message(filename, '');
                  }
                })
                .then(() => task.done());
          });

      audit.run();
    </script>
  </body>
</html>
